/***********************************************************************************************************************
*  OpenStudio(R), Copyright (c) Alliance for Sustainable Energy, LLC.
*  See also https://openstudio.net/license
***********************************************************************************************************************/

#ifndef UTILITIES_GEOMETRY_THREEJS_HPP
#define UTILITIES_GEOMETRY_THREEJS_HPP

#include "../UtilitiesAPI.hpp"

#include "Point3d.hpp"
#include "Transformation.hpp"

#include "../core/Logger.hpp"

#include <vector>
#include <map>
#include <boost/optional.hpp>

namespace Json {
class Value;
}

namespace openstudio {

class ThreeScene;
class ThreeMaterial;

/// enum for materials
enum ThreeSide
{
  FrontSide = 0,
  BackSide = 1,
  DoubleSide = 2
};

/// identifies ThreeJS faces in OpenStudio format (e.g. unlimited number of vertices)
UTILITIES_API unsigned openstudioFaceFormatId();

/// convert RGB to unsigned
UTILITIES_API unsigned toThreeColor(unsigned r, unsigned g, unsigned b);

/// convert string to unsigned
UTILITIES_API unsigned toThreeColor(const std::string& s);

/// convert object name to material name
UTILITIES_API std::string getObjectThreeMaterialName(const std::string& iddObjectType, const std::string& name);

/// convert surface type to material name
UTILITIES_API std::string getSurfaceTypeThreeMaterialName(const std::string& surfaceType);

/// Create a ThreeMaterial
UTILITIES_API ThreeMaterial makeThreeMaterial(const std::string& name, unsigned color, double opacity, unsigned side, unsigned shininess = 50,
                                              const std::string& type = "MeshPhongMaterial");

/// Add a ThreeMaterial to a list of materials and map of material name to material
UTILITIES_API void addThreeMaterial(std::vector<ThreeMaterial>& materials, std::map<std::string, std::string>& materialMap,
                                    const ThreeMaterial& material);

/// Get a material id out of material map
UTILITIES_API std::string getThreeMaterialId(const std::string& materialName, std::map<std::string, std::string>& materialMap);

/// Create the standard ThreeMaterials
UTILITIES_API std::vector<ThreeMaterial> makeStandardThreeMaterials();

/// format a UUID, to limit dependencies UUIDs must be generated outside of this code
UTILITIES_API std::string toThreeUUID(const std::string& uuid);

UTILITIES_API std::string fromThreeUUID(const std::string& uuid);

/// format a list of vertices
UTILITIES_API std::vector<double> toThreeVector(const Point3dVector& vertices);

UTILITIES_API Point3dVector fromThreeVector(const std::vector<double>& vertices);

/// format a Transformation matrix
UTILITIES_API std::vector<double> toThreeMatrix(const Transformation& matrix);

UTILITIES_API Transformation toThreeMatrix(const std::vector<double>& matrix);

/// ThreeGeometryData holds the geometry data for an object
class UTILITIES_API ThreeGeometryData
{
 public:
  ThreeGeometryData(const std::vector<double>& vertices, const std::vector<size_t>& faces);
  std::vector<double> vertices() const;
  std::vector<size_t> normals() const;
  std::vector<size_t> uvs() const;
  std::vector<size_t> faces() const;
  double scale() const;
  bool visible() const;
  bool castShadow() const;
  bool receiveShadow() const;
  bool doubleSided() const;

 private:
  friend class ThreeGeometry;
  ThreeGeometryData(const Json::Value& value);
  Json::Value toJsonValue() const;

  std::vector<double> m_vertices;
  std::vector<size_t> m_normals;
  std::vector<size_t> m_uvs;
  std::vector<size_t> m_faces;
  double m_scale;
  bool m_visible;
  bool m_castShadow;
  bool m_receiveShadow;
  bool m_doubleSided;
};

/// ThreeGeometry holds the geometry for an object
class UTILITIES_API ThreeGeometry
{
 public:
  ThreeGeometry(const std::string& uuid, const ::std::string& type, const ThreeGeometryData& data);
  std::string uuid() const;
  std::string type() const;
  ThreeGeometryData data() const;

 private:
  friend class ThreeScene;
  ThreeGeometry(const Json::Value& value);
  Json::Value toJsonValue() const;

  std::string m_uuid;
  std::string m_type;
  ThreeGeometryData m_data;
};

/// ThreeMaterial defines a rendering material
class UTILITIES_API ThreeMaterial
{
 public:
  ThreeMaterial(const std::string& uuid, const std::string& name, const ::std::string& type, unsigned color, unsigned ambient, unsigned emissive,
                unsigned specular, unsigned shininess, double opacity, bool transparent, bool wireframe, unsigned side);
  std::string uuid() const;
  std::string name() const;
  std::string type() const;
  unsigned color() const;
  unsigned ambient() const;
  unsigned emissive() const;
  unsigned specular() const;
  unsigned shininess() const;
  double opacity() const;
  bool transparent() const;
  bool wireframe() const;
  unsigned side() const;

 private:
  friend class ThreeScene;
  ThreeMaterial(const Json::Value& value);
  Json::Value toJsonValue() const;

  std::string m_uuid;
  std::string m_name;
  std::string m_type;
  unsigned m_color;
  unsigned m_ambient;
  unsigned m_emissive;
  unsigned m_specular;
  unsigned m_shininess;
  double m_opacity;
  bool m_transparent;
  bool m_wireframe;
  unsigned m_side;
};

/// ThreeUserData decorates a ThreeSceneChild with additional information
class UTILITIES_API ThreeUserData
{
 public:
  ThreeUserData();
  std::string handle() const;
  std::string name() const;

  /// surfaceType is overloaded as a more general type:
  /// Surfaces {"Wall", "Floor", "RoofCeiling"}
  /// SubSurfaces {"FixedWindow", "OperableWindow", "GlassDoor", "Skylight", "TubularDaylightDome", "TubularDaylightDiffuser", "Door", "OverheadDoor"}
  /// ShadingSurfaces {"SiteShading", "BuildingShading", "SpaceShading"}
  /// InteriorPartitionSurfaces {"InteriorPartitionSurface"}
  /// DaylightingControl {"DaylightingControl"}
  std::string surfaceType() const;
  std::string surfaceTypeMaterialName() const;

  /// Construction name if any
  std::string constructionName() const;
  std::string constructionHandle() const;
  std::string constructionMaterialName() const;

  /// Parent surface name if any
  std::string surfaceName() const;
  std::string surfaceHandle() const;

  /// Parent sub surface name if any
  std::string subSurfaceName() const;
  std::string subSurfaceHandle() const;

  /// Parent spaces name if any
  std::string spaceName() const;
  std::string spaceHandle() const;

  /// Parent shading name if any
  std::string shadingName() const;
  std::string shadingHandle() const;

  /// ThermalZone name if any
  std::string thermalZoneName() const;
  std::string thermalZoneHandle() const;
  std::string thermalZoneMaterialName() const;

  /// SpaceType name if any
  std::string spaceTypeName() const;
  std::string spaceTypeHandle() const;
  std::string spaceTypeMaterialName() const;

  /// BuildingStory name if any
  std::string buildingStoryName() const;
  std::string buildingStoryHandle() const;
  std::string buildingStoryMaterialName() const;

  /// BuildingUnit name if any
  std::string buildingUnitName() const;
  std::string buildingUnitHandle() const;
  std::string buildingUnitMaterialName() const;

  /// ConstructionSet name if any
  std::string constructionSetName() const;
  std::string constructionSetHandle() const;
  std::string constructionSetMaterialName() const;

  std::string outsideBoundaryCondition() const;
  std::string outsideBoundaryConditionObjectName() const;
  std::string outsideBoundaryConditionObjectHandle() const;
  std::string boundaryMaterialName() const;
  bool coincidentWithOutsideObject() const;
  std::string sunExposure() const;
  std::string windExposure() const;
  double illuminanceSetpoint() const;
  bool airWall() const;

  std::vector<std::string> airLoopHVACNames() const;
  std::vector<std::string> airLoopHVACHandles() const;
  std::vector<std::string> airLoopHVACMaterialNames() const;

  //bool plenum() const;
  //bool belowFloorPlenum() const;
  //bool aboveCeilingPlenum() const;

  void setHandle(const std::string& s);
  void setName(const std::string& s);
  void setSurfaceType(const std::string& s);
  void setSurfaceTypeMaterialName(const std::string& s);
  void setConstructionName(const std::string& s);
  void setConstructionHandle(const std::string& s);
  void setConstructionMaterialName(const std::string& s);
  void setSurfaceName(const std::string& s);
  void setSurfaceHandle(const std::string& s);
  void setSubSurfaceName(const std::string& s);
  void setSubSurfaceHandle(const std::string& s);
  void setSpaceName(const std::string& s);
  void setSpaceHandle(const std::string& s);
  void setShadingName(const std::string& s);
  void setShadingHandle(const std::string& s);
  void setThermalZoneName(const std::string& s);
  void setThermalZoneHandle(const std::string& s);
  void setThermalZoneMaterialName(const std::string& s);
  void setSpaceTypeName(const std::string& s);
  void setSpaceTypeHandle(const std::string& s);
  void setSpaceTypeMaterialName(const std::string& s);
  void setBuildingStoryName(const std::string& s);
  void setBuildingStoryHandle(const std::string& s);
  void setBuildingStoryMaterialName(const std::string& s);
  void setBuildingUnitName(const std::string& s);
  void setBuildingUnitHandle(const std::string& s);
  void setBuildingUnitMaterialName(const std::string& s);
  void setConstructionSetName(const std::string& s);
  void setConstructionSetHandle(const std::string& s);
  void setConstructionSetMaterialName(const std::string& s);
  void setOutsideBoundaryCondition(const std::string& s);
  void setOutsideBoundaryConditionObjectName(const std::string& s);
  void setOutsideBoundaryConditionObjectHandle(const std::string& s);
  void setBoundaryMaterialName(const std::string& s);
  void setCoincidentWithOutsideObject(bool b);
  void setSunExposure(const std::string& s);
  void setWindExposure(const std::string& s);
  void setIlluminanceSetpoint(double d);
  void setAirWall(bool b);
  void addAirLoopHVACName(const std::string& s);
  void addAirLoopHVACHandle(const std::string& s);
  void addAirLoopHVACMaterialName(const std::string& s);
  //void setBelowFloorPlenum(bool v);
  //void setAboveCeilingPlenum(bool v);

  // If set to false, the 4 diags below aren't used
  bool includeGeometryDiagnostics() const;
  void setIncludeGeometryDiagnostics(bool includeGeometryDiagnostics);
  // This Surface is convex
  bool convex() const;
  void setConvex(bool b);
  // The Space it belongs to is convex
  bool spaceConvex() const;
  void setSpaceConvex(bool b);
  // The Space it belongs to is enclosed
  bool spaceEnclosed() const;
  void setSpaceEnclosed(bool b);
  // Given the space it belongs to, it is correctly oriented (or not)
  bool correctlyOriented() const;
  void setCorrectlyOriented(bool b);

 private:
  friend class ThreeSceneChild;
  ThreeUserData(const Json::Value& value);
  Json::Value toJsonValue() const;

  std::string m_handle;
  std::string m_name;
  std::string m_type;
  std::string m_surfaceType;
  std::string m_surfaceTypeMaterialName;
  std::string m_constructionName;
  std::string m_constructionHandle;
  std::string m_constructionMaterialName;
  std::string m_surfaceName;
  std::string m_surfaceHandle;
  std::string m_subSurfaceName;
  std::string m_subSurfaceHandle;
  std::string m_spaceName;
  std::string m_spaceHandle;
  std::string m_shadingName;
  std::string m_shadingHandle;
  std::string m_thermalZoneName;
  std::string m_thermalZoneHandle;
  std::string m_thermalZoneMaterialName;
  std::string m_spaceTypeName;
  std::string m_spaceTypeHandle;
  std::string m_spaceTypeMaterialName;
  std::string m_buildingStoryName;
  std::string m_buildingStoryHandle;
  std::string m_buildingStoryMaterialName;
  std::string m_buildingUnitName;
  std::string m_buildingUnitHandle;
  std::string m_buildingUnitMaterialName;
  std::string m_constructionSetName;
  std::string m_constructionSetHandle;
  std::string m_constructionSetMaterialName;
  std::string m_outsideBoundaryCondition;
  std::string m_outsideBoundaryConditionObjectName;
  std::string m_outsideBoundaryConditionObjectHandle;
  std::string m_boundaryMaterialName;
  bool m_coincidentWithOutsideObject;
  std::string m_sunExposure;
  std::string m_windExposure;
  double m_illuminanceSetpoint;
  bool m_airWall;
  std::vector<std::string> m_airLoopHVACNames;
  std::vector<std::string> m_airLoopHVACHandles;
  std::vector<std::string> m_airLoopHVACMaterialNames;
  //bool m_belowFloorPlenum;
  //bool m_aboveCeilingPlenum;
  bool m_includeGeometryDiagnostics = false;
  bool m_convex = true;
  bool m_spaceConvex = true;
  bool m_spaceEnclosed = true;
  bool m_correctlyOriented = true;
};

/// ThreeSceneChild is a child object of a ThreeSceneObject
class UTILITIES_API ThreeSceneChild
{
 public:
  ThreeSceneChild(const std::string& uuid, const std::string& name, const std::string& type, const std::string& geometryId,
                  const std::string& materialId, const ThreeUserData& userData);
  std::string uuid() const;
  std::string name() const;
  std::string type() const;
  std::string geometry() const;
  std::string material() const;
  std::vector<double> matrix() const;
  ThreeUserData userData() const;

 private:
  friend class ThreeSceneObject;
  ThreeSceneChild(const Json::Value& value);
  Json::Value toJsonValue() const;

  std::string m_uuid;
  std::string m_name;
  std::string m_type;
  std::string m_geometryId;
  std::string m_materialId;
  std::vector<double> m_matrix;
  ThreeUserData m_userData;
};

/// ThreeSceneObject is the root object in a ThreeScene
class UTILITIES_API ThreeSceneObject
{
 public:
  ThreeSceneObject(const std::string& uuid, const std::vector<ThreeSceneChild>& children);
  std::string uuid() const;
  std::string type() const;
  std::vector<double> matrix() const;
  std::vector<ThreeSceneChild> children() const;

 private:
  friend class ThreeScene;
  ThreeSceneObject(const Json::Value& value);
  Json::Value toJsonValue() const;

  std::string m_uuid;
  std::string m_type;
  std::vector<double> m_matrix;
  std::vector<ThreeSceneChild> m_children;
};

/// ThreeBoundingBox includes information about a bounding box
class UTILITIES_API ThreeBoundingBox
{
 public:
  ThreeBoundingBox(double minX, double minY, double minZ, double maxX, double maxY, double maxZ, double lookAtX, double lookAtY, double lookAtZ,
                   double lookAtR);
  double minX() const;
  double minY() const;
  double minZ() const;
  double maxX() const;
  double maxY() const;
  double maxZ() const;
  double lookAtX() const;
  double lookAtY() const;
  double lookAtZ() const;
  double lookAtR() const;

 private:
  friend class ThreeSceneMetadata;
  ThreeBoundingBox(const Json::Value& value);
  Json::Value toJsonValue() const;

  double m_minX;
  double m_minY;
  double m_minZ;
  double m_maxX;
  double m_maxY;
  double m_maxZ;
  double m_lookAtX;
  double m_lookAtY;
  double m_lookAtZ;
  double m_lookAtR;
};

/// ThreeModelObjectMetadata includes metadata about an OpenStudio ModelObject not associated with a ThreeSceneChild in the ThreeJS scene
class UTILITIES_API ThreeModelObjectMetadata
{
 public:
  // DLM: public default ctor seems to be only to make SWIG happy, normal tricks of ignoring vector resize did not work
  // additionally private default ctor with template<class _Ty> friend class std::allocator; also did not work
  ThreeModelObjectMetadata();

  ThreeModelObjectMetadata(const std::string& iddObjectType, const std::string& handle, const std::string& name);
  std::string iddObjectType() const;
  std::string handle() const;
  std::string name() const;

  // applies to general objects
  std::string color() const;
  bool setColor(const std::string& c);
  void resetColor();

  /// applies to Space
  bool openToBelow() const;
  bool setOpenToBelow(bool t);
  void resetOpenToBelow();

  /// applies to Story or Space
  boost::optional<unsigned> multiplier() const;
  bool setMultiplier(unsigned mult);
  void resetMultiplier();

  /// applies to Story
  boost::optional<double> nominalZCoordinate() const;
  bool setNominalZCoordinate(double z);
  void resetNominalZCoordinate();

  /// applies to Story or Space
  boost::optional<double> belowFloorPlenumHeight() const;
  bool setBelowFloorPlenumHeight(double height);
  void resetBelowFloorPlenumHeight();

  /// applies to Story or Space
  boost::optional<double> floorToCeilingHeight() const;
  bool setFloorToCeilingHeight(double height);
  void resetFloorToCeilingHeight();

  /// applies to Story or Space
  boost::optional<double> aboveCeilingPlenumHeight() const;
  bool setAboveCeilingPlenumHeight(double height);
  void resetAboveCeilingPlenumHeight();

 private:
  friend class ThreeSceneMetadata;

  ThreeModelObjectMetadata(const Json::Value& value);
  Json::Value toJsonValue() const;

  std::string m_iddObjectType;
  std::string m_handle;
  std::string m_name;
  std::string m_color;

  bool m_openToBelow;
  boost::optional<unsigned> m_multiplier;
  boost::optional<double> m_nominalZCoordinate;
  boost::optional<double> m_belowFloorPlenumHeight;
  boost::optional<double> m_floorToCeilingHeight;
  boost::optional<double> m_aboveCeilingPlenumHeight;
};

/// ThreeSceneMetadata includes metadata about an OpenStudio Model Object
class UTILITIES_API ThreeSceneMetadata
{
 public:
  ThreeSceneMetadata(const std::vector<std::string>& buildingStoryNames, const ThreeBoundingBox& boundingBox, double northAxis,
                     const std::vector<ThreeModelObjectMetadata>& modelObjectMetadata);
  std::string version() const;
  std::string type() const;
  std::string generator() const;
  std::vector<std::string> buildingStoryNames() const;
  ThreeBoundingBox boundingBox() const;
  double northAxis() const;
  std::vector<ThreeModelObjectMetadata> modelObjectMetadata() const;

 private:
  friend class ThreeScene;
  ThreeSceneMetadata(const Json::Value& value);
  Json::Value toJsonValue() const;

  std::string m_version;
  std::string m_type;
  std::string m_generator;
  std::vector<std::string> m_buildingStoryNames;
  ThreeBoundingBox m_boundingBox;
  double m_northAxis;
  std::vector<ThreeModelObjectMetadata> m_modelObjectMetadata;
};

/** ThreeScene is an adapter for a scene in the three.js geometry format, defined at:
  *   https://github.com/mrdoob/three.js/wiki/JSON-Object-Scene-format-4
  *
  *  The class is not impl-ized in hopes that it can be ported to JavaScript via emscripten
  */
class UTILITIES_API ThreeScene
{
 public:
  /// constructor
  ThreeScene(const ThreeSceneMetadata& metadata, const std::vector<ThreeGeometry>& geometries, const std::vector<ThreeMaterial>& materials,
             const ThreeSceneObject& sceneObject);

  /// constructor from JSON formatted string (or path to a JSON file), will throw if error
  ThreeScene(const std::string& json_str);

  /// load from string
  static boost::optional<ThreeScene> load(const std::string& json);

  /// print to JSON
  std::string toJSON(bool prettyPrint = false) const;

  ThreeSceneMetadata metadata() const;
  std::vector<ThreeGeometry> geometries() const;
  boost::optional<ThreeGeometry> getGeometry(const std::string& geometryId) const;
  std::vector<ThreeMaterial> materials() const;
  boost::optional<ThreeMaterial> getMaterial(const std::string& materialId) const;

  /** This method is renamed to 'threeSceneObject()' in C# */
  ThreeSceneObject object() const;

 private:
  REGISTER_LOGGER("ThreeScene");

  ThreeSceneMetadata m_metadata;
  std::vector<ThreeGeometry> m_geometries;
  std::vector<ThreeMaterial> m_materials;
  ThreeSceneObject m_sceneObject;
};

}  // namespace openstudio

#endif  //UTILITIES_GEOMETRY_THREEJS_HPP
